好好学习,天天向上

译|比一比:Python的七个数据可视化工具

原文:Comparing 7 Python data visualization tools

Python的科学栈相当成熟。目前已经有许多用于各种各样目的的库,包括机器学习数据分析。数据可视化是能够探索数据和交流结果的重要组成部分,但是在过去,稍微落后于其他工具,例如,R。

幸运的是,在过去的几年里,许多新的Python数据可视化库被创造出来以缩小差距。matplotlib已经成为主要的数据可视化库,但是,也有其他诸如vispy, bokeh, seaborn, pygal, foliumnetworkx的库,它们要么建立在matplotlib的基础上,要么具备matplotlib所不支持的功能。

在这篇文章中,我们将使用一个真实世界的数据集,然后使用这些库进行可视化。在这个过程中,我们会发现什么领域使用什么库最好,以及如何最有效的利用Python的数据可视化生态系统。

Dataquest中,我们已经建立了一个互动课程。这个课程会教你Python数据可视化工具。如果你想更深入的了解,看看这里

探索数据集

在我们深入数据可视化之前,让我们快速浏览一下将使用的数据集。我们将使用来自openflights的数据。我们还会使用(航线)[http://openflights.org/data.html#route], 机场航空公司的数据。航线数据中的每一行对应于两个机场之间的一条航空公司航线。机场数据中的每一行对应世界上的一个机场及其相关信息。航空公司数据的每一行表示一个航空公司。

注:openflights这个网站可能打不开。此时,可以使用github上的数据:openflights

我们首先读入数据:

1
2
3
4
5
6
7
8
9
10
11
# 导入pandas库
import pandas
# 读入机场数据
airports = pandas.read_csv("airports.csv", header=None, dtype=str)
airports.columns = ["id", "name", "city", "country", "code", "icao", "latitude", "longitude", "altitude", "offset", "dst", "timezone"]
# 读入航空公司数据
airlines = pandas.read_csv("airlines.csv", header=None, dtype=str)
airlines.columns = ["id", "name", "alias", "iata", "icao", "callsign", "country", "active"]
# 读入航线数据
routes = pandas.read_csv("routes.csv", header=None, dtype=str)
routes.columns = ["airline", "airline_id", "source", "source_id", "dest", "dest_id", "codeshare", "stops", "equipment"]
该数据不包含列标题,因此我们通过指定columns属性添加列标题。我们想将每一列作为字符串读取 - 这会使得后面当我们想基于id匹配每一行时,比较整个数据帧更加容易,因此,我们在读取数据时设置dtype参数。

我们可以快速浏览一下每个数据帧:

1
airports.head()
| | id | name | city | country | code | icao | latitude | longitude | altitude | offset | dst | timezone | | -------- | -----: | :----: | -----: | :----: | -----: | :----: | -----: | :----: | -----: | :----: | -----: | :----: | | 0 | 1 | Goroka | Goroka | Papua New Guinea | GKA | AYGA | -6.081689 | 145.391881 | 5282 | 10 | U | Pacific/Port_Moresby | | 1 | 2 | Madang | Madang | Papua New Guinea | MAG | AYMD | -5.207083 | 145.788700 | 20 | 10 | U | Pacific/Port_Moresby | | 2 | 3 | Mount Hagen | Mount Hagen | Papua New Guinea | HGU | AYMH | -5.826789 | 144.295861 | 5388 | 10 | U | Pacific/Port_Moresby | | 3 | 4 | Nadzab | Nadzab | Papua New Guinea | LAE | AYNZ | -6.569828 | 146.726242 | 239 | 10 | U | Pacific/Port_Moresby | | 4 | 5 | Port Moresby Jacksons Intl | Port Moresby | Papua New Guinea | POM | AYPY | -9.443383 | 147.220050 | 146 | 10 | U | Pacific/Port_Moresby |

1
airlines.head()
id name alias iata icao callsign country active
0 1 Private flight - NaN NaN NaN Y
1 2 135 Airways NaN GNL GENERAL United States N
2 3 1Time Airline 1T RNX NEXTIME South Africa Y
3 4 2 Sqn No 1 Elementary Flying Training School NaN WYT NaN United Kingdom N
4 5 213 Flight Unit NaN TFU NaN Russia N
1
routes.head()
airline airline_id source source_id dest dest_id codeshare stops equipment
0 2B 410 AER 2965 KZN 2990 NaN 0 CR2
1 2B 410 ASF 2966 KZN 2990 NaN 0 CR2
2 2B 410 ASF 2966 MRV 2962 NaN 0 CR2
3 2B 410 CEK 2968 KZN 2990 NaN 0 CR2
4 2B 410 CEK 2968 OVB 4078 NaN 0 CR2

我们可以对每一个数据集单独的做各种有趣的探索,但是通过将它们结合起来,我们将会看到最大的收获。在进行分析的时候,Pandas会帮助我们,因为它能够很容易的过滤矩阵或在它们上面应用函数。我们将深入一些有趣的度量,例如分析航空公司和路线。

在此之前,需要进行一些数据清理:

1
routes = routes[routes["airline_id"] != "\\N"]
上面的代码确保在airline_id列只有数字数据。

建立直方图

现在,我们已经了解了数据的结构,可以继续前进,开始画图来探索它们。对于第一个图,我们将使用matplotlib。matplotlib是Python栈中一个相对较低级别的绘图库,因此它比其他库需要更多的命令来绘制一个漂亮的图。另一方面,你可以使用matplotlib来绘制几乎任何种类的图。它非常灵活,但这种灵活性是以冗余为代价的。

我们首先绘制直方图来显示按航空公司划分的航线长度分布情况。直方图把所有的航线长度划分成范围(或"仓库"),然后统计每个范围中有多少路线。这可以告诉我们,航空公司飞行更短的航线,还是更长的航线。

为了做到这一点,我们需要首先计算航线的长度。第一步是一个距离公式。我们将使用Haversine距离,即经纬度对之间的距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import math

def haversine(lon1, lat1, lon2, lat2):
# 将坐标转换为浮点数
lon1, lat1, lon2, lat2 = [float(lon1), float(lat1), float(lon2), float(lat2)]
# 将度数转换为弧度
lon1, lat1, lon2, lat2 = map(math.radians, [lon1, lat1, lon2, lat2])
# 计算距离
dlon = lon2 - lon1
dlat = lat2 - lat1
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
c = 2 * math.asin(math.sqrt(a))
km = 6367 * c
return km

然后,我们可以编写一个函数来计算源机场和目的机场之间某条航线的距离。要做到这一点,我们需要从航线数据帧中获得机场的source_iddest_id,然后根据机场数据帧中的id列将它们匹配起来以获得这些机场的经度和纬度。接下来,就只是单纯的计算了。下面是这个函数:

1
2
3
4
5
6
7
8
9
10
11
def calc_dist(row):
dist = 0
try:
# 匹配源和目的以获得坐标
source = airports[airports["id"] == row["source_id"]].iloc[0]
dest = airports[airports["id"] == row["dest_id"]].iloc[0]
# 使用坐标计算距离
dist = haversine(dest["longitude"], dest["latitude"], source["longitude"], source["latitude"])
except (ValueError, IndexError):
pass
return dist

如果source_id或者dest_id列出现无效值时,这个函数将运行失败,所以我们要增加try/except块来捕捉。

最后,我们要使用pandas对整个routes数据帧应用距离计算函数。它将为我们提供一个包含所有航线长度的pandas series。所有航线的长度都是以千米为单位。

1
route_lengths = routes.apply(calc_dist, axis=1)
现在,我们得到了航线的长度,可以创建一个直方图来将这些值归入各个范围中,然后计算每个范围中有多少航线:
1
2
3
4
import matplotlib.pyplot as plt
%matplotlib inline

plt.hist(route_lengths, bins=20)

使用import matplotlib.pyplot as plt导入matplotlib绘图函数。然后使用%matplotlib inline在ipython notebook中启动matplotlib显示图形。最后,使用plt.hist(route_lengths, bins=20)创建一个直方图。正如我们所看到的,相对于长航线,航空公司飞行更多的短航线。

使用Seaborn

我们可以使用seaborn(一个更高级的Python绘图库)绘制出相似的图。Seaborn基于matplotlib,创建某些类型的图,通常用于统计工作会更简单。我们可以使用distplot函数来绘制一个直方图,这个图上会带有一个内核密度评估。一个内核密度评估是一个曲线 - 实质上是直方图的一个平滑版本,它更易于看到模式。

1
2
import seaborn
seaborn.distplot(route_lengths, bins=20)

正如你所看到的,seaborn拥有比matplotlib更好的默认样式。Seaborn并没有所有matplotlib图形的它自己的版本,但是相比较默认的matplotlib图表,它是一种快速获得更深入好看的图形的很好的方式。如果你需要更深入,并且进行一些统计工作,它也是一个很好的库。

柱状图

直方图很棒,但也许我们想要按航空公司查看平均航线长度。我们可以改用柱状图 - 每一家航空公司都有一个单独的柱表示平均航线长度。这将让我们看到哪些运营商是区域性的,哪些是国际化的。我们可以使用pandas,一个python数据分析库,来计算每一个航空公司的平均航线长度。

1
2
3
4
5
6
7
8
import numpy

# 将相关列放入一个数据帧中
route_length_df = pandas.DataFrame({"length": route_lengths, "id": routes["airline_id"]})
# 计算每一个航空公司的平均航线长度
airline_route_lengths = route_length_df.groupby("id").aggregate(numpy.mean)
# 根据长度排序,这样我们可以得到一个更好的图表
airline_route_lengths = airline_route_lengths.sort("length", ascending=False)

首先,我们使用航线长度和航空公司的id来创建一个新的数据帧。接着基于airline_idroute_length_df分组,从根本上使得每一家航空公司创建一个数据帧。然后使用pandas的aggregate函数计算每一个航空公司数据帧的length列的平均值,再将每一个结果结合成一个新的数据帧。然后,我们对数据帧进行排序以使得最长航线长度的航空公司出现在第一位。

现在,我们可以使用matplotlib画图了:

1
plt.bar(range(airline_route_lengths.shape[0]), airline_route_lengths["length"])

matplotlib的plt.bar方法绘制每一个航空公司飞行的平均航线长度(airline_route_lengths["length"]).

上面的图形的问题是,我们不能很容易的看出每一个航线长度对应哪一个航空公司。为了达到这个目的,我们需要能够看到轴标签。这有点困难,因为有这么多家航空公司。使这更容易的一个方法是将图形变成交互式的,这样将允许我们放大和缩小以查看标签。我们可以使用bokeh库来达到此目的 - 它可以很简单的创建可互动可缩放的图形。

要使用bokeh,我们需要先预处理我们的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def lookup_name(row):
try:
# 将row的id域航空公司数据帧中的id进行匹配,这样我们可以获得航空公司的名字.
name = airlines["name"][airlines["id"] == row["id"]].iloc[0]
except (ValueError, IndexError):
name = ""
return name

# 将索引(航空公司的id)作为一个列添加
airline_route_lengths["id"] = airline_route_lengths.index.copy()
# 查找所有航空公司的名字
airline_route_lengths["name"] = airline_route_lengths.apply(lookup_name, axis=1)
# 删除索引中的重复值
airline_route_lengths.index = range(airline_route_lengths.shape[0])

上面的代码将为airline_route_lengths中的每一行获取名称,然后将其保存到name列(包含每一家航空公司的名字)。我们也增加id列,这样我们能进行这种查找(apply函数不用传递索引)。

最后,我们重置索引列来获得所有唯一值。若非如此,Bokeh无法正常工作。

现在,我们可以继续画图了:

1
2
3
4
5
6
7
import numpy as np
from bokeh.io import output_notebook
from bokeh.charts import Bar, show

output_notebook()
p = Bar(airline_route_lengths, 'name', values='length', title='Average airline route lengths')
show(p)
调用output_notebook设置bokeh可以在ipython notebook中显示图表。然后,使用我们的数据帧和对应的列创建一个条形图。最后,show函数显示这个图表。

这个图表并不是一张图 - 它其实是一个javascript小部件。正因为如此,我们在下面展示的是一张截图,而不是实际的图表。

使用它,我们可以放大然后查看哪一个航空公司飞行最长的距离。上面的图使得这些标签看起来很拥挤,但是随着被放大,它们会容易看得多。

水平条形图

Pygal是一个Python数据分析库,它可以快速的制作出吸引人的图表。我们可以使用它来根据长度对航线进行分类。首先,将航线分为短期,中期和长期,然后计算它们每一个在route_lengths的百分比。

1
2
3
long_routes = len([k for k in route_lengths if k > 10000]) / len(route_lengths)
medium_routes = len([k for k in route_lengths if k < 10000 and k > 2000]) / len(route_lengths)
short_routes = len([k for k in route_lengths if k < 2000]) / len(route_lengths)

然后,在pygal水平条形图中,将它们每一个绘制成一个条形:

1
2
3
4
5
6
7
8
9
10
import pygal
from IPython.display import SVG

chart = pygal.HorizontalBar()
chart.title = 'Long, medium, and short routes'
chart.add('Long', long_routes*100)
chart.add('Medium', medium_routes*100)
chart.add('Short', short_routes*100)
chart.render_to_file('routes.svg')
SVG(filename='routes.svg')

在上面,我们首先创造了一个空图表。然后,添加元素,包括标题和条形。每一个条形对应一个百分比,表示航线类型有多常见。

最后,绘制图表文件,并使用IPython中的SVG显示功能加载和显示文件。此图看起来比默认的matplotlib图表漂亮许多,但是我们也需要写更多的代码来创建它。Pygal可能比较适用于小型演示图形。

散点图

散点图能让我们比较数据列。我们可以制作一个简单的散点图来比较航空公司id值及其名字的长度:

1
2
name_lengths = airlines['name'].apply(lambda x: len(str(x)))
plt.scatter(airlines['id'].astype(int), name_lengths)

首先,通通过pandas的apply方法计算每一个名字的长度。这将查找每一个航空公司名字的字母数。

然后,使用matplotlib制作一张散点图来对航空公司的id及其名字的长度进行比较。在绘图的时候,将airlinesid列转换成整型。如果不这样做,将无法进行绘图,因为在x轴,它需要数字值。可以看到,越早的id拥有越长的名字。这可能意味着,较早成立的航空公司往往有较长的名称。

我们可以使用seaborn来验证这个说法。seaborn有一个散点图的增强版本,一个联合图表,它显示了两个变量之间的关系,以及每一个的单独分布。

1
2
data = pandas.DataFrame({"lengths": name_lengths, "ids": airlines["id"].astype(int)})
seaborn.jointplot(x="ids", y="lengths", data=data)

上面的图表表明,两个变量之间不存在任何真实的相关性 - R平方值低。

静态地图

我们的数据就本质而言是相当适合绘制地图的 - 机场、源机场和目的地机场都具有经纬度对。

我们可以制作的第一张图是一张显示遍布全世界的所有机场。我们可以使用basemap这基于matplotlib扩展的工具。它可以绘制世界地图,添加点,而且非常个性化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 导入basemap包
from mpl_toolkits.basemap import Basemap

# 创建一个地图用于绘制。
# 我们使用的是墨卡托投影,并显示整个世界。
m = Basemap(projection='merc',llcrnrlat=-80,urcrnrlat=80,llcrnrlon=-180,urcrnrlon=180,lat_ts=20,resolution='c')
# 绘制海岸线,以及地图的边缘
m.drawcoastlines()
m.drawmapboundary()
# 将经纬度转换成x和y坐标
x, y = m(list(airports["longitude"].astype(float)), list(airports["latitude"].astype(float)))
# 使用matplotlib在地图上绘制点
m.scatter(x,y,1,marker='o',color='red')
# 显示图表
plt.show()

在上面的代码中,我们首先使用墨卡托投影绘制世界地图。墨卡托投影是一种将整个世界投影成二维表面的方法。然后,使用红点在地图上绘制机场。

上面地图的问题是很难看出来每一个机场在哪里 - 它们在高机场密度区域会合并成一个红色的斑点。

正如使用bokeh一样,也有一个交互式的地图库,叫做folium,可以用来放大地图以帮助我们查找单个机场。

1
2
3
4
5
6
7
8
9
10
11
12
import folium

# 获得一个基本世界地图
airports_map = folium.Map(location=[30, 0], zoom_start=2)
# 在上面绘制标记
for name, row in airports.iterrows():
# 由于某些原因,这个机场会引发问题
if row["name"] != "South Pole Station":
airports_map.circle_marker(location=[row["latitude"], row["longitude"]], popup=row["name"])
# 创建并显示地图
airports_map.create_map('airports.html')
airports_map

folium使用leaflet.js来制作一个完全可交互的地图。你可以点击每一个机场来查看弹出的名字。上面是截图,但是真实的地图会更加的令人印象深刻。folium也允许你修改范围非常广泛的选项来制作更好的标记,或者添加更多的东西到地图中。

绘制大圆圈(great circle)

看到所有航线显示在一张地图上将是一件相当酷的事情。幸运的是,我们可以使用basemap来做到这一点。我们将绘制链接源和目的地机场的大圆。每个圆圈将显示一个单一的客机路线。不幸的是,有那么多的路线以至于将它们全部显示出来会乱七八糟。相反,我们将显示前3000条路线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 使用墨卡托投影制作一张基础地图。绘制海岸线。
m = Basemap(projection='merc',llcrnrlat=-80,urcrnrlat=80,llcrnrlon=-180,urcrnrlon=180,lat_ts=20,resolution='c')
m.drawcoastlines()

# 遍历前3000行数据
for name, row in routes[:3000].iterrows():
try:
# 获取源和目的地机场
source = airports[airports["id"] == row["source_id"]].iloc[0]
dest = airports[airports["id"] == row["dest_id"]].iloc[0]
# 不要绘制太长的航线。
if abs(float(source["longitude"]) - float(dest["longitude"])) < 90:
# 在源和目的机场之间绘制一个大圆圈。
m.drawgreatcircle(float(source["longitude"]), float(source["latitude"]), float(dest["longitude"]), float(dest["latitude"]),linewidth=1,color='b')
except (ValueError, IndexError):
pass

# 显示地图
plt.show()

上面的代码将会绘制一张地图,然后在上面绘制航线。我们增加了一些过滤以防止过长的航线模糊了其他航线。

绘制网络图

我们将进行的最后的探索是绘制一张机场的网络图。每一个机场将会是网络中的一个结点,而如果机场之间存在一条航线,我们将绘制结点之间的边。如果有多条航线,将增加边的权重,以表明机场的连接成都。我们将使用networkx库来做到这点。

首先,我们需要计算机场之间的边的权重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 初始化权重字典
weights = {}
# 跟踪已经添加过一次的键 -- 我们只想要超过1权重的边来使得我们的网络规模可控。
added_keys = []
# 遍历每一条航线
for name, row in routes.iterrows():
# 抽取源和目的地id
source = row["source_id"]
dest = row["dest_id"]

# 为权重字典创建一个键
# 这对应一条边,拥有航线的起点和终点。
key = "{0}_{1}".format(source, dest)
# 如果键已经在wieghts中了,增加权重。
if key in weights:
weights[key] += 1
# 如果key在已添加键中,用权重值2初始化weights字典中的键
elif key in added_keys:
weights[key] = 2
# 如果键不在added_keys中,添加它。
# 这确保我们并未添加权重值为1的边
else:
added_keys.append(key)
一旦上面的代码结束运行,权重字典会包含两个机场之间的权重值超过1的边。所以任何一个被两个及以上的航线连接的机场将会展示出来。

现在,我们需要绘图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 导入networkx,初始化图形
import networkx as nx
graph = nx.Graph()
# 在这个集合中追踪已添加的结点,以避免我们添加两次
nodes = set()
# 遍历每一条边
for k, weight in weights.items():
try:
# 分隔源和目的地id,并把它们转换成整型
source, dest = k.split("_")
source, dest = [int(source), int(dest)]
# 若源不在nodes中,则添加它
if source not in nodes:
graph.add_node(source)
# 若目的地不在nodes中,则添加它
if dest not in nodes:
graph.add_node(dest)
# 将源和目的地都添加到nodes集合中。
# 集合不允许重复
nodes.add(source)
nodes.add(dest)

# 将边添加到图上
graph.add_edge(source, dest, weight=weight)
except (ValueError, IndexError):
pass

pos=nx.spring_layout(graph)

# 绘制结点和边
nx.draw_networkx_nodes(graph,pos, node_color='red', node_size=10, alpha=0.8)
nx.draw_networkx_edges(graph,pos,width=1.0,alpha=1)

# 显示图形
plt.show()

总结

已经产生了大量用于数据可视化的Python库,这使得制作几乎任何类型的可视化成为可能。大多数的库是基于matplotlib,并让某些用例更加简单。如果你想要更深入了解如何使用matplotlib, seaborn和其他工具进行数据可视化,看看我们的互动课程

请言小午吃个甜筒~~